﻿using System.Collections.Generic;
using Common;
using System;

namespace Gestures
{
    public class Transformer
    {

        private int resamplingRate;
        private float transformAngle;
        private Point scaleFactor;

        public Transformer() {
            resamplingRate = 64;
            scaleFactor = new Point(1.0f, 1.0f);
            transformAngle = 0.0f;
        }


        public Point getScaleFactor()
        {
            return new Point(scaleFactor.x, scaleFactor.y);
        }


        public void setResamplingRate(int r)
        {
            resamplingRate = r;
        }


        public int getResamplingRate()
        {
            return resamplingRate;
        }


        public float getTransformAngle()
        {
            return transformAngle;
        }

        
        public List<float> getDistances(List<Point> l)
        {
            if (l == null || l.Count < 2)
            {
                throw new ArgumentException("Cannot calculate length of empty list.");
            }
            List<float> distances = new List<float>(l.Count - 1);
            // Calculate distance between all consecutive points
            for (int i = 0; i < l.Count - 1; ++i)
            {
                float dx = l[i + 1].x - l[i].x;
                float dy = l[i + 1].y - l[i].y;
                distances.Add((float) Math.Sqrt(dx * dx + dy * dy));
            }
            return distances;
        }


        public List<Point> resample(List<Point> l)
        {
            // TODO: assertion checking in plaats van throwing.
            if (l == null || l.Count < 2)
            {
                throw new ArgumentException("Cannot resample empty list.");
            }

            List<float> distances = getDistances(l);

            float totalLength = 0.0f;

            // Calculate total path length
            for (int i = 0; i < distances.Count; ++i)
            {
                totalLength += distances[i];
            }

            // Resample points in original list to be at equal distances.
            // According to paper 32 < x < 256 sample points are fine.
            // I use Resamplerate - 1 because 64 sample points equal 63 line segments.
            float sampleDistance = totalLength / (float)(resamplingRate - 1);
            List<Point> resampled = new List<Point>(resamplingRate - 1);

            float currentDistance = 0.0f;
            int distanceIndex = 0;
            int samplesAdded = 0;
            while (samplesAdded < resamplingRate)
            {
                // First we remove all distance segments that we 'passed' already.
                while (distanceIndex < distances.Count && currentDistance >= distances[distanceIndex])
                {
                    currentDistance -= distances[distanceIndex];
                    distanceIndex++;
                }

                if (distanceIndex == distances.Count)
                {
                    // We are on the end of the line, insert the final point.
                    Point p = new Point(l[distanceIndex].x, l[distanceIndex].y);
                    resampled.Add(p);
                }
                else
                {
                    // We have to interpolate between the two old points.
                    float relativeDistance = currentDistance / distances[distanceIndex];
                    float dx = l[distanceIndex + 1].x - l[distanceIndex].x;
                    float dy = l[distanceIndex + 1].y - l[distanceIndex].y;
                    float interpolatedx = l[distanceIndex].x + dx * relativeDistance;
                    float interpolatedy = l[distanceIndex].y + dy * relativeDistance;
                    resampled.Add(new Point(interpolatedx, interpolatedy));
                }

                samplesAdded++;
                currentDistance += sampleDistance;
            }

            return resampled;
        }


        public Point calculateCenter(List<Point> l)
        {
            // TODO: assertion checking in plaats van throwing.
            if (l == null || l.Count < 1)
            {
                throw new ArgumentException("Cannot calculate center of empty list.");
            }

            Point center = new Point(0.0f, 0.0f);

            for (int i = 0; i < l.Count; ++i)
            {
                center.x += l[i].x;
                center.y += l[i].y;
            }

            center.x /= l.Count;
            center.y /= l.Count;

            return center;
        }


        public float getWidth(List<Point> l)
        {
            if (l == null || l.Count < 2)
            {
                throw new ArgumentException("Cannot calculate width of empty list.");
            }

            float min = float.MaxValue, max = float.MinValue;
            for (int i = 0; i < l.Count; ++i)
            {
                min = Math.Min(l[i].x, min);
                max = Math.Max(l[i].x, max);
            }
            return max - min;
        }


        public float getHeight(List<Point> l)
        {
            if (l == null || l.Count < 2)
            {
                throw new ArgumentException("Cannot calculate height of empty list.");
            }

            float min = float.MaxValue, max = float.MinValue;
            for (int i = 0; i < l.Count; ++i)
            {
                min = Math.Min(l[i].y, min);
                max = Math.Max(l[i].y, max);
            }
            return max - min;
        }


        public float calculateIndicativeAngle(List<Point> l, Point center)
        {
            if (l == null || l.Count < 2)
            {
                throw new ArgumentException("Cannot calculate indicative angle of empty list.");
            }

            return (float)Math.Atan2(center.y - l[0].y, center.x - l[0].x);
        }


        public void rotateInPlace(List<Point> l, Point center, float angle)
        {
            // TODO: roteert dit wel de goede kant op?
            if (center == null || l == null)
            {
                throw new ArgumentNullException("Cannot rotate");
            }

            float sin = (float)Math.Sin(angle);
            float cos = (float)Math.Cos(angle);

            for (int i = 0; i < l.Count; ++i)
            {
                float newx = (l[i].x - center.x) * cos - (l[i].y - center.y) * sin + center.x;
                float newy = (l[i].x - center.x) * sin + (l[i].y - center.y) * cos + center.y;

                l[i].x = newx;
                l[i].y = newy;
            }
        }



        public List<Point> rotate(List<Point> l, Point center, float angle)
        {
            List<Point> newList = copy(l);
            rotateInPlace(newList, center, angle);
            return newList;
        }


        public List<Point> copy(List<Point> l)
        {
            if (l == null)
            {
                throw new ArgumentNullException();
            }
            List<Point> newList = new List<Point>(l.Count);
            for (int i = 0; i < l.Count; ++i)
            {
                // TODO: kopie maken kan vast efficienter
                newList.Add(new Point(l[i].x, l[i].y));
            }
            return newList;
        }


        public List<Point> translate(List<Point> l, Point translation)
        {
            List<Point> newList = copy(l);
            translateInPlace(newList, translation);
            return newList;
        }


        public void translateInPlace(List<Point> l, Point translation)
        {
            if (l == null || translation == null)
            {
                throw new ArgumentNullException();
            }

            for (int i = 0; i < l.Count; ++i)
            {
                l[i].x += translation.x;
                l[i].y += translation.y;
            }
        }

        public List<Point> transform(List<Point> l)
        {
            return transform(l, false);
        }

        public List<Point> transform(List<Point> l, bool uniform)
        {
            if (l == null || l.Count < 2)
            {
                throw new ArgumentException("Cannot transform empty list.");
            }

            List<Point> resampled = resample(l);            
            
            // Rotate the gesture around its center.
            Point center = calculateCenter(resampled);
            transformAngle = calculateIndicativeAngle(resampled, center);

              // rotate in such a way that the indicative angle is zero.
            rotateInPlace(resampled, center, -transformAngle);
            
            // Translate in such a way that the center of the gesture is at (0, 0)
            center.x = 0 - center.x;
            center.y = 0 - center.y;
            translateInPlace(resampled, center);
           
            // Normalize the gesture to [-0.5, 0.5] in all directions
            // TODO: I do non-uniform scaling.
            // Does this removes information, for example square vs. rectangle?
            normalizeInPlace(resampled, uniform);
            return resampled;
        }


        public void normalizeInPlace(List<Point> l, bool uniform)
        {
            if (l == null)
            {
                throw new ArgumentException("Cannot scale empty list");
            }

            float width = getWidth(l);
            float height = uniform? width : getHeight(l);

            // Only scale two-dimensional sets
            width = width == 0.0f ? 1.0f : 1.0f / width;
            height = height == 0.0f ? 1.0f : 1.0f / height;       

            scaleFactor = new Point(width, height);
            scaleInPlace(l, scaleFactor);
        }


        public List<Common.Point> scale(List<Common.Point> l, Point scale)
        {
            List<Common.Point> newList = copy(l);
            scaleInPlace(newList, scale);
            return newList;
        }


        public void scaleInPlace(List<Point> l, Point scale)
        {
            if (l == null || scale == null)
            {
                throw new ArgumentException("Cannot scale, invalid arguments.");
            }

            for (int i = 0; i < l.Count; ++i)
            {
                l[i].x *= scale.x;
                l[i].y *= scale.y;
            }
        }

        public void appendInPlace(List<Point> l1, List<Point> l2, float angle)
        {
            if (l1 == null || l2 == null)
            {
                throw new ArgumentException("Cannot append: null lists.");
            }

            if (l1.Count == 0) {
                throw new ArgumentException("Cannot append to empty list.");
            }

            if (l2.Count < 2)
            {
                return;
            }

            // translate in such a way that the first point of l2 is at (0, 0).
            Point firstPoint = new Point(-l2[0].x, -l2[0].y);
            List<Point> translated = translate(l2, firstPoint);

            // rotate the second set of points according to the given angle.
            rotateInPlace(translated, Common.Point.Zero, angle);

            // and translate it to "fit" at the last point of the previous list
            translateInPlace(translated, l1[l1.Count - 1]);

            // copy all points. Skip the first point since it is always located at (0,0).
            for (int i = 1; i < translated.Count; ++i)
            {
                l1.Add(translated[i]);
            }
        }

        public List<Point> append(List<Point> l1, List<Point> l2, float angle) {
            List<Point> newList = copy(l1);
            appendInPlace(newList, l2, angle);
            return newList;
        }
    }
}